﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.Windows.Forms;
using System.Xml;
using DynCalc;
using TreeNode=System.Windows.Forms.TreeNode;

namespace StructureTree
{
    public interface ITreeEventHandler
    {
        void SelectItem(FormatNode fnode);
        byte[] GetValue(int offset, int size);
    }

    public partial class StructureTree : UserControl
    {
        private ITreeEventHandler _EventHandler;

        public StructureTree()
        {
            InitializeComponent();
        }

        public bool LoadXmlFile(string fileName)
        {
            try 
            {
                // SECTION 1. Create a DOM Document and load the XML data into it.
                //
                XmlDocument dom = new XmlDocument();
                dom.Load(fileName);

                // Validate the signature
                //
                XmlElement root = dom.DocumentElement;
                if (root.Attributes["signature"] != null) {
                    if (root.Attributes["signature_offset"] == null) {
                        throw new Exception(
                            "signature attribute is defined, but signature_offset is not defined.\nPlease make sure that the signature_offset attribute is defined");
                    }

                    byte[] value = ConvertValueToByte(root.Attributes["signature"].Value);
                    int offset = int.Parse(root.Attributes["signature_offset"].Value);

                    byte[] binary_value = _EventHandler.GetValue(offset, value.Length);

                    bool error = false;
                    if (value.Length == binary_value.Length) {
                        for (int pos = 0 ;pos < value.Length; pos++) {
                            if (value[pos] != binary_value[pos]) {
                                error = true;
                            }
                        }
                    }
                    else {
                        error = true;
                    }

                    if (error) {
                        MessageBox.Show("Invalid file format. Signature is not identical.", "Error", MessageBoxButtons.OK,
                                        MessageBoxIcon.Error);
                        return false;
                    }
                }

                // SECTION 2. Initialize the TreeView control.
                //
                treeView.Nodes.Clear();
                treeView.Nodes.Add(new TreeNode(dom.DocumentElement.Name));
                TreeNode tNode = new TreeNode();
                tNode.ImageIndex = 0;
                tNode = treeView.Nodes[0];

                // SECTION 3. Populate the TreeView with the DOM nodes.
                //
                tNode.Name = dom.DocumentElement.Name;
                AddNode(dom.DocumentElement, tNode, 0);
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message, "Exception", MessageBoxButtons.OK, MessageBoxIcon.Error);
                treeView.Nodes.Clear();
                return false;
            }

            return true;
        }

        private byte[] ConvertValueToByte(string value)
        {
            Debug.Assert(value != null);

            if (value.IndexOf("0x") == 0) {     // Hex
                int size = (value.Length - 2)/2;
                byte[] ret = new byte[size];

                for (int i = 0; i < size; i++) {
                    ret[i] = (byte) int.Parse(value.Substring(i*2 + 2, 2), NumberStyles.AllowHexSpecifier); ;
                }

                return ret;
            }
            else {
                throw new Exception("Signature should be hex\nFor example, 0x424D\n\nCurrent signature: " + value);
            }
        }


        private void AddNode(XmlNode inXmlNode, TreeNode inTreeNode, int offset)
        {
            XmlNode xNode;
            TreeNode tNode;
            XmlNodeList nodeList;
            int start_offset = offset;
            int inOffset = offset;

            if (inXmlNode.Attributes["offset"] != null)
            {
                string strValue = inXmlNode.Attributes["offset"].Value;

                inOffset = CalculateValue(strValue);
            }

            // Loop through the XML nodes until the leaf is reached.
            // Add the nodes to the TreeView during the looping process.
            if (inXmlNode.HasChildNodes)
            {
                int size = 0;

                // offset 값을 직접 가져오도록 한다.
                //
                int childOffset = inOffset;

                nodeList = inXmlNode.ChildNodes;
                int node_count = 0;

                int i;
                for (i = 0; i <= nodeList.Count - 1; i++)
                {
                    if (inXmlNode.ChildNodes[node_count].NodeType != XmlNodeType.Element)
                        continue;

                    xNode = inXmlNode.ChildNodes[node_count];

                    // Repeat check
                    //
                    if (xNode.Name == "FM_COMMAND_REPEAT")
                    {
                        int count = CalculateValue(xNode.Attributes["count"].Value);

                        int node_offset = inTreeNode.Nodes.Count;
                        childOffset = ((FormatNode)inTreeNode.Nodes[node_offset - 1].Tag).Offset + ((FormatNode)inTreeNode.Nodes[node_offset - 1].Tag).Size;

                        for (int repeat = 0; repeat < count; repeat++)
                        {
                            inTreeNode.Nodes.Add(new TreeNode(xNode.ChildNodes[0].Name + "(" + repeat + ")"));

                            TreeNode curNode = inTreeNode.Nodes[node_offset + repeat];
                            curNode.Name = inTreeNode.Name + "\\" + xNode.ChildNodes[0].Name + "(" + repeat + ")";

                            AddNode(xNode.ChildNodes[0], curNode, childOffset);
                            curNode.ImageIndex = (curNode.Nodes.Count == 0) ? 2 : 0;
                            curNode.SelectedImageIndex = curNode.ImageIndex;

                            childOffset += ((FormatNode)curNode.Tag).Size;
                            size += ((FormatNode) curNode.Tag).Size;
                        }
                    }
                    else
                    {
                        inTreeNode.Nodes.Add(new TreeNode(xNode.Name));
                        tNode = inTreeNode.Nodes[node_count];
                        tNode.Name = inTreeNode.Name + "\\" + xNode.Name;

                        AddNode(xNode, tNode, childOffset);
                        tNode.ImageIndex = (tNode.Nodes.Count == 0) ? 2 : 0;
                        tNode.SelectedImageIndex = tNode.ImageIndex;

                        // 상위 노드의 경우, 하위 노드의 합으로 정의할 수 있다.
                        // 이 때 상위 노드에 size가 정의되어 있는 경우에는
                        // 상위 노드의 size 값을 그대로 사용한다.
                        //
                        size += ((FormatNode)tNode.Tag).Size;
                        childOffset += ((FormatNode)tNode.Tag).Size;
                    }

                    node_count++;
                }

                SetNodeItem(inXmlNode, inTreeNode, inOffset);

                if (inXmlNode.Attributes["size"] == null)
                {
                    // 부모 노드의 크기 값은 자식 노드의 Size의 합이 아니라,
                    // 이전 offset과 최종 offset의 차이로 계산한다.
                    // 왜냐하면 자식 노드가 중간에 빈 공간을 가질 수 있기 때문이다.
                    //
                    ((FormatNode)inTreeNode.Tag).Size = childOffset - start_offset ;
                }
            }
            else
            {
                SetNodeItem(inXmlNode, inTreeNode, inOffset);
            }
        }

        private int CalculateValue(string str)
        {
            int ret = 0;

            string expr = str;
            char[] charSeparators = new char[] {' '};
            string[] indirectVariables = str.Replace('(', ' ').Replace(')', ' ').Replace('+', ' ').Replace('-', ' ').Replace('*', ' ').Replace('/', ' ').Replace('%', ' ').Split(charSeparators, StringSplitOptions.RemoveEmptyEntries);

            foreach(string var in indirectVariables)
            {
                int value = -1;
                var.Trim(' ');

                switch(var[0])
                {
                    case '!':   // Size
                        value = FindPathAndReturnSize(var.TrimStart('!'));
                        break;
                    case '@':   // Offset
                        value = FindPathAndReturnOffset(var.TrimStart('@'));
                        break;
                    case '#':   // Hex Value
                        value = FindPathAndReturnValue(var.TrimStart('#'));
                        break;
                    default:
                        value = Convert.ToInt32(var);
                        break;
                }

                expr = expr.Replace(var, value.ToString());
            }


            // LearnFromCode: Calculate relative variables
            // Source Code from http://community.bartdesmet.net/blogs/bart/
            //
            // 음수로 시작하면 오류 발생!
            //
            Queue<MathOpOrVal> q = Expression.InfixToPostfix(expr);
            DynCalc.TreeNode tree = DynCalc.Program.ToTree(q);
            ret = DynCalc.Program.Execute(tree);

            return ret;
        }

        private int FindPathAndReturnValue(string str)
        {
            TreeNode node = FindPath(treeView.Nodes[0], str);
            if (node == null) {
                throw new Exception("Invalid expression\n\n" + str + "\n\n" + "Cannot find the tree node.");
            }

            int offset = ((FormatNode)node.Tag).Offset;
            int size = ((FormatNode) node.Tag).Size;
            byte[] value = _EventHandler.GetValue(offset, size);
            if (value == null) {
                throw new Exception("Out of range!\nThe value of the following expression couldn't be handled properly because the size is larger than the size of the binary file.\nPlease make sure that the expression is correct\n\n" + str);
            }
            offset = 0;

            for ( int i = size-1 ;i >= 0 ; i--)
            {
                offset = offset << 8;
                offset += value[i];
            }

            return offset;
        }

        private int FindPathAndReturnSize(string str)
        {
            TreeNode node = FindPath(treeView.Nodes[0], str);
            if (node == null) {
                throw new Exception("Invalid expression\n\n" + str + "\n\n" + "Cannot find the tree node.");
            }

            return ((FormatNode)node.Tag).Size;
        }

        private int FindPathAndReturnOffset(string str)
        {
            TreeNode node = FindPath(treeView.Nodes[0], str);
            if (node == null) {
                throw new Exception("Invalid expression\n\n" + str + "\n\n" + "Cannot find the tree node.");
            }
            
            return ((FormatNode)node.Tag).Offset;
        }

        private TreeNode FindPath(TreeNode inTreeNode, string str)
        {
            Debug.Assert(inTreeNode != null);

            int nodeCount = inTreeNode.GetNodeCount(false);

            if (nodeCount != 0)
            {
                foreach (TreeNode childNode in inTreeNode.Nodes)
                {
                    TreeNode node = FindPath(childNode, str);
                    if ( node != null )
                        return node;
                }
            }
            if (inTreeNode.Tag == null)
                return null;

            if (str == ((FormatNode)inTreeNode.Tag).Path)
                return inTreeNode;
            else
                return null;
        }

        private void SetNodeItem(XmlNode xmlNode, TreeNode treeNode, int offset)
        {
            FormatNode fnode = new FormatNode();

            if (xmlNode.Attributes["size"] != null)
            {
                fnode.Size = CalculateValue(xmlNode.Attributes["size"].Value);
            }

            if (xmlNode.Attributes["comment"] != null)
            {
                fnode.Comment = xmlNode.Attributes["comment"].Value;    
            }

            if (xmlNode.Attributes["name"] != null)
            {
                treeNode.Text = xmlNode.Attributes["name"].Value;
            }

            fnode.Text = treeNode.Text;
            fnode.Path = treeNode.Name;

            fnode.Offset = offset;
            treeNode.Tag = fnode;

            // SetToolTip(treeNode);
        }

        private void SetToolTip(TreeNode treeNode)
        {
            FormatNode fnode = ((FormatNode)treeNode.Tag);

            treeNode.ToolTipText = "full name: " + treeNode.Name + "\nsize: " + fnode.Size + "\noffset: " + fnode.Offset + "\ncomment: " + fnode.Comment;
        }

        public void SetEventHandler (ITreeEventHandler eventHandler)
        {
            _EventHandler = eventHandler;
        }

        private void treeView_AfterSelect(object sender, TreeViewEventArgs e)
        {
            FormatNode fnode = (FormatNode)e.Node.Tag;
            _EventHandler.SelectItem(fnode);
            this.Focus();
        }

        public void SelectItemByOffset(int offset)
        {
            // offset이 특정 영역에 포함되어 있는지를 확인한다.
            TreeNode node = FindItemByOffset(treeView.Nodes[0], offset);

            if (node != null)
            {
                treeView.SelectedNode = node;
                _EventHandler.SelectItem((FormatNode)node.Tag);
                this.Focus();
            }
        }

        private TreeNode FindItemByOffset(TreeNode inTreeNode, int offset)
        {
            // offset을 포함하고 있는 최하위 노드를 선택해야 하기 때문에
            // offset보다 큰 값이 나오게 되면 return 한다.
            //
            Debug.Assert(inTreeNode != null);

            FormatNode fnode = (FormatNode)inTreeNode.Tag;
            if (offset >= (fnode.Size + fnode.Offset))
                return null;

            int nodeCount = inTreeNode.GetNodeCount(false);

            if (nodeCount != 0)
            {
                //TreeNode retNode = null;

                foreach (TreeNode childNode in inTreeNode.Nodes)
                {
                    TreeNode node = FindItemByOffset(childNode, offset);
                    if (node != null)
                        return node;
                }
            }
            else
            {
                if (offset <= (fnode.Size + fnode.Offset))
                {
                    return inTreeNode;
                }
            }
            return null;            
        }
    }

    [DefaultPropertyAttribute("Item")]
    public class FormatNode
    {
        private string _Text;
        private string _Path; 
        private string _Comment;
        private int _Size;
        private int _Offset;

        [CategoryAttribute("Description"), DescriptionAttribute("Name of the item")]
        public string Text
        {
            set { _Text = value; }
            get { return _Text; }
        }

        [CategoryAttribute("Description"), DescriptionAttribute("Path of the item including all the parent items separated by slash(/)")]
        public string Path
        {
            set { _Path = value; }
            get { return _Path; }
        }

        [CategoryAttribute("Description"), DescriptionAttribute("Description of the item")]
        public string Comment
        {
            set { _Comment = value; }
            get { return _Comment; }
        }

        [CategoryAttribute("Data"), DescriptionAttribute("Offset of the item from the start address")]
        public int Offset
        {
            set { _Offset = value; }
            get { return _Offset; }
        }

        [CategoryAttribute("Data"), DescriptionAttribute("Size of the item (bytes)")]
        public int Size
        {
            set { _Size = value; }
            get { return _Size; }
        }
    }
}
 